/*
 * MIT License
 *
 * Copyright (c) 2022 zycra
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.gitee.zycra.jdbc.common;

import com.gitee.zycra.jdbc.model.PageQueryWrapper;
import com.gitee.zycra.jdbc.model.PageResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

/**
 * SQL execute util for complex SQL or multi table join query SQL.
 *
 * @author zycra
 * @since 1.0.0
 */
@Repository
public class CommonSelector {

    /**
     * Slf4j logger.
     *
     * @since 1.0.0
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(CommonSelector.class);

    /**
     * Native global print SQL log switch, default is false.
     *
     * @since 1.0.0
     */
    @Value("${spring.datasource.print-sql:false}")
    private boolean printSQL;

    /**
     * Native print SQL log format.
     *
     * @since 1.0.0
     */
    private static final String SQL_LOG_TEXT_BLOCK = """
            sql execute log
            ------------------------------------------------------------------------
            cost:   {}ms
            sql:    {};
            result: {}
            ------------------------------------------------------------------------
            """;

    /**
     * Select one row result by given params.
     *
     * @param jdbcTemplate jdbcTemplate instance.
     * @param sql          SQL to execute.
     * @param clazz        result data object type class.
     * @param paramList    query params.
     * @param <T>          result data object type.
     * @return one row result.
     * @since 1.0.0
     */
    public <T> T selectOneByParam(JdbcTemplate jdbcTemplate, String sql, Class<T> clazz, List<Object> paramList) {
        return selectOneByParam(jdbcTemplate, sql, clazz, paramList, printSQL);
    }

    /**
     * Select one row result by given params.
     *
     * @param jdbcTemplate jdbcTemplate instance.
     * @param sql          SQL to execute.
     * @param clazz        result data object type class.
     * @param paramList    query params.
     * @param printSQL     whether print SQL log.
     * @param <T>          result data object type.
     * @return one row result.
     * @since 1.0.0
     */
    public <T> T selectOneByParam(JdbcTemplate jdbcTemplate, String sql, Class<T> clazz, List<Object> paramList, boolean printSQL) {
        checkParam(jdbcTemplate, sql, clazz);
        RowMapper<T> rowMapper = DataObjectContainer.getRowMapper(clazz);
        T result = null;
        try {
            long start = System.currentTimeMillis();
            if (rowMapper == null) {
                result = CollectionUtils.isEmpty(paramList)
                        ? jdbcTemplate.queryForObject(sql, clazz)
                        : jdbcTemplate.queryForObject(sql, clazz, paramList.toArray());
                printDebugSqlLog(System.currentTimeMillis() - start, sql, paramList, result, printSQL);
                return result;
            }
            result = CollectionUtils.isEmpty(paramList)
                    ? jdbcTemplate.queryForObject(sql, rowMapper)
                    : jdbcTemplate.queryForObject(sql, rowMapper, paramList.toArray());
            printDebugSqlLog(System.currentTimeMillis() - start, sql, paramList, result, printSQL);
        } catch (EmptyResultDataAccessException ignored) {
            // ignored
        }
        return result;
    }

    /**
     * Select multi row results by given params.
     *
     * @param jdbcTemplate jdbcTemplate instance.
     * @param sql          SQL to execute.
     * @param clazz        result data object type class.
     * @param paramList    query params.
     * @param <T>          result data object type.
     * @return multi row results.
     * @since 1.0.0
     */
    public <T> List<T> selectByParam(JdbcTemplate jdbcTemplate, String sql, Class<T> clazz, List<Object> paramList) {
        return selectByParam(jdbcTemplate, sql, clazz, paramList, printSQL);
    }

    /**
     * Select multi row results by given params.
     *
     * @param jdbcTemplate jdbcTemplate instance.
     * @param sql          SQL to execute.
     * @param clazz        result data object type class.
     * @param paramList    query params.
     * @param printSQL     whether print SQL log.
     * @param <T>          result data object type.
     * @return multi row results.
     * @since 1.0.0
     */
    public <T> List<T> selectByParam(JdbcTemplate jdbcTemplate, String sql, Class<T> clazz, List<Object> paramList, boolean printSQL) {
        checkParam(jdbcTemplate, sql, clazz);
        RowMapper<T> rowMapper = DataObjectContainer.getRowMapper(clazz);
        List<T> result = null;
        try {
            long start = System.currentTimeMillis();
            if (rowMapper == null) {
                result = CollectionUtils.isEmpty(paramList)
                        ? jdbcTemplate.queryForList(sql, clazz)
                        : jdbcTemplate.queryForList(sql, clazz, paramList.toArray());
                printDebugSqlLog(System.currentTimeMillis() - start, sql, paramList, result, printSQL);
                return result;
            }
            result = CollectionUtils.isEmpty(paramList)
                    ? jdbcTemplate.query(sql, rowMapper)
                    : jdbcTemplate.query(sql, rowMapper, paramList.toArray());
            printDebugSqlLog(System.currentTimeMillis() - start, sql, paramList, result, printSQL);
        } catch (EmptyResultDataAccessException ignored) {
            // ignored
        }
        return result;
    }

    /**
     * Select page results by given params.
     *
     * @param jdbcTemplate jdbcTemplate instance.
     * @param clazz        result data object type class.
     * @param param        query param.
     * @param <T>          result data object type.
     * @return page results.
     * @since 1.0.0
     */
    public <T> PageResult<T> selectForPage(JdbcTemplate jdbcTemplate, Class<T> clazz, PageQueryWrapper param) {
        return selectForPage(jdbcTemplate, clazz, param, printSQL);
    }

    /**
     * Select page results by given params.
     *
     * @param jdbcTemplate jdbcTemplate instance.
     * @param clazz        result data object type class.
     * @param param        query param.
     * @param printSQL     whether print SQL log.
     * @param <T>          result data object type.
     * @return page results.
     * @since 1.0.0
     */
    public <T> PageResult<T> selectForPage(JdbcTemplate jdbcTemplate, Class<T> clazz, PageQueryWrapper param, boolean printSQL) {
        checkParam(jdbcTemplate, clazz, param);
        long start = System.currentTimeMillis();
        Integer total = jdbcTemplate.queryForObject(param.getCountTotalSql(), Integer.class, param.getCountTotalSqlParam().toArray());
        printDebugSqlLog(System.currentTimeMillis() - start, param.getCountTotalSql(), param.getCountTotalSqlParam(), total, printSQL);
        Assert.notNull(total, "total is null");
        if (total.equals(0)) {
            return new PageResult<>(1, 0, new ArrayList<>());
        }
        int page = param.getPage();
        int size = param.getSize();
        int maxPage = total % size == 0 ? total / size : total / size + 1;
        page = Math.min(page, maxPage);
        RowMapper<T> rowMapper = DataObjectContainer.getRowMapper(clazz);
        String sql = param.getQuerySql() + " LIMIT ?, ?";
        List<Object> paramList = new ArrayList<>(param.getQuerySqlParam());
        paramList.add((page - 1) * size);
        paramList.add(size);
        start = System.currentTimeMillis();
        List<T> data = rowMapper == null
                ? jdbcTemplate.queryForList(sql, clazz, paramList.toArray())
                : jdbcTemplate.query(sql, rowMapper, paramList.toArray());
        printDebugSqlLog(System.currentTimeMillis() - start, sql, paramList, data, printSQL);
        return new PageResult<>(page, total, data);
    }

    /**
     * Check param validated.
     *
     * @param jdbcTemplate jdbcTemplate instance.
     * @param sql          SQL to execute.
     * @param clazz        data object type class.
     * @param <T>          data object type.
     * @since 1.0.0
     */
    private <T> void checkParam(JdbcTemplate jdbcTemplate, String sql, Class<T> clazz) {
        Assert.notNull(jdbcTemplate, "jdbcTemplate is null");
        Assert.notNull(sql, "sql is null");
        Assert.notNull(clazz, "clazz is null");
    }

    /**
     * Check param validated.
     *
     * @param jdbcTemplate jdbcTemplate instance.
     * @param clazz        data object type class.
     * @param param        page query param.
     * @param <T>          data object type.
     * @since 1.0.0
     */
    private <T> void checkParam(JdbcTemplate jdbcTemplate, Class<T> clazz, PageQueryWrapper param) {
        Assert.notNull(jdbcTemplate, "jdbcTemplate is null");
        Assert.notNull(clazz, "clazz is null");
        Assert.notNull(param, "param is null");
    }

    /**
     * Print native SQL log.
     *
     * @param costTime   the cost time for execute the sql.
     * @param sql        executed SQL.
     * @param paramList  param list of SQL.
     * @param result     executed result.
     * @param printSQL   whether print current SQL log.
     * @since 1.0.0
     */
    void printDebugSqlLog(long costTime, String sql, List<Object> paramList, Object result, boolean printSQL) {
        if (!printSQL) {
            return;
        }
        if (CollectionUtils.isEmpty(paramList)) {
            LOGGER.info(SQL_LOG_TEXT_BLOCK, costTime, sql, result);
            return;
        }
        for (Object o : paramList) {
            String block = String.valueOf(o);
            if (o instanceof String || o instanceof LocalDateTime || o instanceof LocalDate) {
                block = "'" + block + "'";
            }
            sql = sql.replaceFirst("\\?", block);
        }
        LOGGER.info(SQL_LOG_TEXT_BLOCK, costTime, sql, result);
    }
}
